/*
 * Copyright (c) 1997, 2013, Oracle and/or its affiliates. All rights reserved.
 * ORACLE PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 */

/*
 **********************************************************************
 **********************************************************************
 **********************************************************************
 *** COPYRIGHT (c) Eastman Kodak Company, 1997                      ***
 *** As  an unpublished  work pursuant to Title 17 of the United    ***
 *** States Code.  All rights reserved.                             ***
 **********************************************************************
 **********************************************************************
 **********************************************************************/

package java.awt.image;

import java.awt.Point;
import java.awt.Graphics2D;
import java.awt.color.*;
import sun.java2d.cmm.ColorTransform;
import sun.java2d.cmm.CMSManager;
import sun.java2d.cmm.ProfileDeferralMgr;
import sun.java2d.cmm.PCMM;
import java.awt.geom.Rectangle2D;
import java.awt.geom.Point2D;
import java.awt.RenderingHints;

/**
 * This class performs a pixel-by-pixel color conversion of the data in
 * the source image.  The resulting color values are scaled to the precision
 * of the destination image.  Color conversion can be specified
 * via an array of ColorSpace objects or an array of ICC_Profile objects.
 * <p>
 * If the source is a BufferedImage with premultiplied alpha, the
 * color components are divided by the alpha component before color conversion.
 * If the destination is a BufferedImage with premultiplied alpha, the
 * color components are multiplied by the alpha component after conversion.
 * Rasters are treated as having no alpha channel, i.e. all bands are
 * color bands.
 * <p>
 * If a RenderingHints object is specified in the constructor, the
 * color rendering hint and the dithering hint may be used to control
 * color conversion.
 * <p>
 * Note that Source and Destination may be the same object.
 *
 * @see java.awt.RenderingHints#KEY_COLOR_RENDERING
 * @see java.awt.RenderingHints#KEY_DITHERING
 */
public class ColorConvertOp implements BufferedImageOp, RasterOp {

  ICC_Profile[] profileList;
  ColorSpace[] CSList;
  ColorTransform thisTransform, thisRasterTransform;
  ICC_Profile thisSrcProfile, thisDestProfile;
  RenderingHints hints;
  boolean gotProfiles;
  float[] srcMinVals, srcMaxVals, dstMinVals, dstMaxVals;

  /* the class initializer */
  static {
    if (ProfileDeferralMgr.deferring) {
      ProfileDeferralMgr.activateProfiles();
    }
  }

  /**
   * Constructs a new ColorConvertOp which will convert
   * from a source color space to a destination color space.
   * The RenderingHints argument may be null.
   * This Op can be used only with BufferedImages, and will convert
   * directly from the ColorSpace of the source image to that of the
   * destination.  The destination argument of the filter method
   * cannot be specified as null.
   *
   * @param hints the <code>RenderingHints</code> object used to control the color conversion, or
   * <code>null</code>
   */
  public ColorConvertOp(RenderingHints hints) {
    profileList = new ICC_Profile[0];    /* 0 length list */
    this.hints = hints;
  }

  /**
   * Constructs a new ColorConvertOp from a ColorSpace object.
   * The RenderingHints argument may be null.  This
   * Op can be used only with BufferedImages, and is primarily useful
   * when the {@link #filter(BufferedImage, BufferedImage) filter}
   * method is invoked with a destination argument of null.
   * In that case, the ColorSpace defines the destination color space
   * for the destination created by the filter method.  Otherwise, the
   * ColorSpace defines an intermediate space to which the source is
   * converted before being converted to the destination space.
   *
   * @param cspace defines the destination <code>ColorSpace</code> or an intermediate
   * <code>ColorSpace</code>
   * @param hints the <code>RenderingHints</code> object used to control the color conversion, or
   * <code>null</code>
   * @throws NullPointerException if cspace is null
   */
  public ColorConvertOp(ColorSpace cspace, RenderingHints hints) {
    if (cspace == null) {
      throw new NullPointerException("ColorSpace cannot be null");
    }
    if (cspace instanceof ICC_ColorSpace) {
      profileList = new ICC_Profile[1];    /* 1 profile in the list */

      profileList[0] = ((ICC_ColorSpace) cspace).getProfile();
    } else {
      CSList = new ColorSpace[1]; /* non-ICC case: 1 ColorSpace in list */
      CSList[0] = cspace;
    }
    this.hints = hints;
  }


  /**
   * Constructs a new ColorConvertOp from two ColorSpace objects.
   * The RenderingHints argument may be null.
   * This Op is primarily useful for calling the filter method on
   * Rasters, in which case the two ColorSpaces define the operation
   * to be performed on the Rasters.  In that case, the number of bands
   * in the source Raster must match the number of components in
   * srcCspace, and the number of bands in the destination Raster
   * must match the number of components in dstCspace.  For BufferedImages,
   * the two ColorSpaces define intermediate spaces through which the
   * source is converted before being converted to the destination space.
   *
   * @param srcCspace the source <code>ColorSpace</code>
   * @param dstCspace the destination <code>ColorSpace</code>
   * @param hints the <code>RenderingHints</code> object used to control the color conversion, or
   * <code>null</code>
   * @throws NullPointerException if either srcCspace or dstCspace is null
   */
  public ColorConvertOp(ColorSpace srcCspace, ColorSpace dstCspace,
      RenderingHints hints) {
    if ((srcCspace == null) || (dstCspace == null)) {
      throw new NullPointerException("ColorSpaces cannot be null");
    }
    if ((srcCspace instanceof ICC_ColorSpace) &&
        (dstCspace instanceof ICC_ColorSpace)) {
      profileList = new ICC_Profile[2];    /* 2 profiles in the list */

      profileList[0] = ((ICC_ColorSpace) srcCspace).getProfile();
      profileList[1] = ((ICC_ColorSpace) dstCspace).getProfile();

      getMinMaxValsFromColorSpaces(srcCspace, dstCspace);
    } else {
            /* non-ICC case: 2 ColorSpaces in list */
      CSList = new ColorSpace[2];
      CSList[0] = srcCspace;
      CSList[1] = dstCspace;
    }
    this.hints = hints;
  }


  /**
   * Constructs a new ColorConvertOp from an array of ICC_Profiles.
   * The RenderingHints argument may be null.
   * The sequence of profiles may include profiles that represent color
   * spaces, profiles that represent effects, etc.  If the whole sequence
   * does not represent a well-defined color conversion, an exception is
   * thrown.
   * <p>For BufferedImages, if the ColorSpace
   * of the source BufferedImage does not match the requirements of the
   * first profile in the array,
   * the first conversion is to an appropriate ColorSpace.
   * If the requirements of the last profile in the array are not met
   * by the ColorSpace of the destination BufferedImage,
   * the last conversion is to the destination's ColorSpace.
   * <p>For Rasters, the number of bands in the source Raster must match
   * the requirements of the first profile in the array, and the
   * number of bands in the destination Raster must match the requirements
   * of the last profile in the array.  The array must have at least two
   * elements or calling the filter method for Rasters will throw an
   * IllegalArgumentException.
   *
   * @param profiles the array of <code>ICC_Profile</code> objects
   * @param hints the <code>RenderingHints</code> object used to control the color conversion, or
   * <code>null</code>
   * @throws IllegalArgumentException when the profile sequence does not specify a well-defined
   * color conversion
   * @throws NullPointerException if profiles is null
   */
  public ColorConvertOp(ICC_Profile[] profiles, RenderingHints hints) {
    if (profiles == null) {
      throw new NullPointerException("Profiles cannot be null");
    }
    gotProfiles = true;
    profileList = new ICC_Profile[profiles.length];
    for (int i1 = 0; i1 < profiles.length; i1++) {
      profileList[i1] = profiles[i1];
    }
    this.hints = hints;
  }


  /**
   * Returns the array of ICC_Profiles used to construct this ColorConvertOp.
   * Returns null if the ColorConvertOp was not constructed from such an
   * array.
   *
   * @return the array of <code>ICC_Profile</code> objects of this <code>ColorConvertOp</code>, or
   * <code>null</code> if this <code>ColorConvertOp</code> was not constructed with an array of
   * <code>ICC_Profile</code> objects.
   */
  public final ICC_Profile[] getICC_Profiles() {
    if (gotProfiles) {
      ICC_Profile[] profiles = new ICC_Profile[profileList.length];
      for (int i1 = 0; i1 < profileList.length; i1++) {
        profiles[i1] = profileList[i1];
      }
      return profiles;
    }
    return null;
  }

  /**
   * ColorConverts the source BufferedImage.
   * If the destination image is null,
   * a BufferedImage will be created with an appropriate ColorModel.
   *
   * @param src the source <code>BufferedImage</code> to be converted
   * @param dest the destination <code>BufferedImage</code>, or <code>null</code>
   * @return <code>dest</code> color converted from <code>src</code> or a new, converted
   * <code>BufferedImage</code> if <code>dest</code> is <code>null</code>
   * @throws IllegalArgumentException if dest is null and this op was constructed using the
   * constructor which takes only a RenderingHints argument, since the operation is ill defined.
   */
  public final BufferedImage filter(BufferedImage src, BufferedImage dest) {
    ColorSpace srcColorSpace, destColorSpace;
    BufferedImage savdest = null;

    if (src.getColorModel() instanceof IndexColorModel) {
      IndexColorModel icm = (IndexColorModel) src.getColorModel();
      src = icm.convertToIntDiscrete(src.getRaster(), true);
    }
    srcColorSpace = src.getColorModel().getColorSpace();
    if (dest != null) {
      if (dest.getColorModel() instanceof IndexColorModel) {
        savdest = dest;
        dest = null;
        destColorSpace = null;
      } else {
        destColorSpace = dest.getColorModel().getColorSpace();
      }
    } else {
      destColorSpace = null;
    }

    if ((CSList != null) ||
        (!(srcColorSpace instanceof ICC_ColorSpace)) ||
        ((dest != null) &&
            (!(destColorSpace instanceof ICC_ColorSpace)))) {
            /* non-ICC case */
      dest = nonICCBIFilter(src, srcColorSpace, dest, destColorSpace);
    } else {
      dest = ICCBIFilter(src, srcColorSpace, dest, destColorSpace);
    }

    if (savdest != null) {
      Graphics2D big = savdest.createGraphics();
      try {
        big.drawImage(dest, 0, 0, null);
      } finally {
        big.dispose();
      }
      return savdest;
    } else {
      return dest;
    }
  }

  private final BufferedImage ICCBIFilter(BufferedImage src,
      ColorSpace srcColorSpace,
      BufferedImage dest,
      ColorSpace destColorSpace) {
    int nProfiles = profileList.length;
    ICC_Profile srcProfile = null, destProfile = null;

    srcProfile = ((ICC_ColorSpace) srcColorSpace).getProfile();

    if (dest == null) {        /* last profile in the list defines
                                      the output color space */
      if (nProfiles == 0) {
        throw new IllegalArgumentException(
            "Destination ColorSpace is undefined");
      }
      destProfile = profileList[nProfiles - 1];
      dest = createCompatibleDestImage(src, null);
    } else {
      if (src.getHeight() != dest.getHeight() ||
          src.getWidth() != dest.getWidth()) {
        throw new IllegalArgumentException(
            "Width or height of BufferedImages do not match");
      }
      destProfile = ((ICC_ColorSpace) destColorSpace).getProfile();
    }

        /* Checking if all profiles in the transform sequence are the same.
         * If so, performing just copying the data.
         */
    if (srcProfile == destProfile) {
      boolean noTrans = true;
      for (int i = 0; i < nProfiles; i++) {
        if (srcProfile != profileList[i]) {
          noTrans = false;
          break;
        }
      }
      if (noTrans) {
        Graphics2D g = dest.createGraphics();
        try {
          g.drawImage(src, 0, 0, null);
        } finally {
          g.dispose();
        }

        return dest;
      }
    }

        /* make a new transform if needed */
    if ((thisTransform == null) || (thisSrcProfile != srcProfile) ||
        (thisDestProfile != destProfile)) {
      updateBITransform(srcProfile, destProfile);
    }

        /* color convert the image */
    thisTransform.colorConvert(src, dest);

    return dest;
  }

  private void updateBITransform(ICC_Profile srcProfile,
      ICC_Profile destProfile) {
    ICC_Profile[] theProfiles;
    int i1, nProfiles, nTransforms, whichTrans, renderState;
    ColorTransform[] theTransforms;
    boolean useSrc = false, useDest = false;

    nProfiles = profileList.length;
    nTransforms = nProfiles;
    if ((nProfiles == 0) || (srcProfile != profileList[0])) {
      nTransforms += 1;
      useSrc = true;
    }
    if ((nProfiles == 0) || (destProfile != profileList[nProfiles - 1]) ||
        (nTransforms < 2)) {
      nTransforms += 1;
      useDest = true;
    }

        /* make the profile list */
    theProfiles = new ICC_Profile[nTransforms]; /* the list of profiles
                                                       for this Op */

    int idx = 0;
    if (useSrc) {
            /* insert source as first profile */
      theProfiles[idx++] = srcProfile;
    }

    for (i1 = 0; i1 < nProfiles; i1++) {
                                   /* insert profiles defined in this Op */
      theProfiles[idx++] = profileList[i1];
    }

    if (useDest) {
            /* insert dest as last profile */
      theProfiles[idx] = destProfile;
    }

        /* make the transform list */
    theTransforms = new ColorTransform[nTransforms];

        /* initialize transform get loop */
    if (theProfiles[0].getProfileClass() == ICC_Profile.CLASS_OUTPUT) {
                                        /* if first profile is a printer
                                           render as colorimetric */
      renderState = ICC_Profile.icRelativeColorimetric;
    } else {
      renderState = ICC_Profile.icPerceptual; /* render any other
                                                       class perceptually */
    }

    whichTrans = ColorTransform.In;

    PCMM mdl = CMSManager.getModule();

        /* get the transforms from each profile */
    for (i1 = 0; i1 < nTransforms; i1++) {
      if (i1 == nTransforms - 1) {         /* last profile? */
        whichTrans = ColorTransform.Out; /* get output transform */
      } else {      /* check for abstract profile */
        if ((whichTrans == ColorTransform.Simulation) &&
            (theProfiles[i1].getProfileClass() ==
                ICC_Profile.CLASS_ABSTRACT)) {
          renderState = ICC_Profile.icPerceptual;
          whichTrans = ColorTransform.In;
        }
      }

      theTransforms[i1] = mdl.createTransform(
          theProfiles[i1], renderState, whichTrans);

            /* get this profile's rendering intent to select transform
               from next profile */
      renderState = getRenderingIntent(theProfiles[i1]);

            /* "middle" profiles use simulation transform */
      whichTrans = ColorTransform.Simulation;
    }

        /* make the net transform */
    thisTransform = mdl.createTransform(theTransforms);

        /* update corresponding source and dest profiles */
    thisSrcProfile = srcProfile;
    thisDestProfile = destProfile;
  }

  /**
   * ColorConverts the image data in the source Raster.
   * If the destination Raster is null, a new Raster will be created.
   * The number of bands in the source and destination Rasters must
   * meet the requirements explained above.  The constructor used to
   * create this ColorConvertOp must have provided enough information
   * to define both source and destination color spaces.  See above.
   * Otherwise, an exception is thrown.
   *
   * @param src the source <code>Raster</code> to be converted
   * @param dest the destination <code>WritableRaster</code>, or <code>null</code>
   * @return <code>dest</code> color converted from <code>src</code> or a new, converted
   * <code>WritableRaster</code> if <code>dest</code> is <code>null</code>
   * @throws IllegalArgumentException if the number of source or destination bands is incorrect, the
   * source or destination color spaces are undefined, or this op was constructed with one of the
   * constructors that applies only to operations on BufferedImages.
   */
  public final WritableRaster filter(Raster src, WritableRaster dest) {

    if (CSList != null) {
            /* non-ICC case */
      return nonICCRasterFilter(src, dest);
    }
    int nProfiles = profileList.length;
    if (nProfiles < 2) {
      throw new IllegalArgumentException(
          "Source or Destination ColorSpace is undefined");
    }
    if (src.getNumBands() != profileList[0].getNumComponents()) {
      throw new IllegalArgumentException(
          "Numbers of source Raster bands and source color space " +
              "components do not match");
    }
    if (dest == null) {
      dest = createCompatibleDestRaster(src);
    } else {
      if (src.getHeight() != dest.getHeight() ||
          src.getWidth() != dest.getWidth()) {
        throw new IllegalArgumentException(
            "Width or height of Rasters do not match");
      }
      if (dest.getNumBands() !=
          profileList[nProfiles - 1].getNumComponents()) {
        throw new IllegalArgumentException(
            "Numbers of destination Raster bands and destination " +
                "color space components do not match");
      }
    }

        /* make a new transform if needed */
    if (thisRasterTransform == null) {
      int i1, whichTrans, renderState;
      ColorTransform[] theTransforms;

            /* make the transform list */
      theTransforms = new ColorTransform[nProfiles];

            /* initialize transform get loop */
      if (profileList[0].getProfileClass() == ICC_Profile.CLASS_OUTPUT) {
                                            /* if first profile is a printer
                                               render as colorimetric */
        renderState = ICC_Profile.icRelativeColorimetric;
      } else {
        renderState = ICC_Profile.icPerceptual; /* render any other
                                                           class perceptually */
      }

      whichTrans = ColorTransform.In;

      PCMM mdl = CMSManager.getModule();

            /* get the transforms from each profile */
      for (i1 = 0; i1 < nProfiles; i1++) {
        if (i1 == nProfiles - 1) {         /* last profile? */
          whichTrans = ColorTransform.Out; /* get output transform */
        } else {  /* check for abstract profile */
          if ((whichTrans == ColorTransform.Simulation) &&
              (profileList[i1].getProfileClass() ==
                  ICC_Profile.CLASS_ABSTRACT)) {
            renderState = ICC_Profile.icPerceptual;
            whichTrans = ColorTransform.In;
          }
        }

        theTransforms[i1] = mdl.createTransform(
            profileList[i1], renderState, whichTrans);

                /* get this profile's rendering intent to select transform
                   from next profile */
        renderState = getRenderingIntent(profileList[i1]);

                /* "middle" profiles use simulation transform */
        whichTrans = ColorTransform.Simulation;
      }

            /* make the net transform */
      thisRasterTransform = mdl.createTransform(theTransforms);
    }

    int srcTransferType = src.getTransferType();
    int dstTransferType = dest.getTransferType();
    if ((srcTransferType == DataBuffer.TYPE_FLOAT) ||
        (srcTransferType == DataBuffer.TYPE_DOUBLE) ||
        (dstTransferType == DataBuffer.TYPE_FLOAT) ||
        (dstTransferType == DataBuffer.TYPE_DOUBLE)) {
      if (srcMinVals == null) {
        getMinMaxValsFromProfiles(profileList[0],
            profileList[nProfiles - 1]);
      }
            /* color convert the raster */
      thisRasterTransform.colorConvert(src, dest,
          srcMinVals, srcMaxVals,
          dstMinVals, dstMaxVals);
    } else {
            /* color convert the raster */
      thisRasterTransform.colorConvert(src, dest);
    }

    return dest;
  }

  /**
   * Returns the bounding box of the destination, given this source.
   * Note that this will be the same as the the bounding box of the
   * source.
   *
   * @param src the source <code>BufferedImage</code>
   * @return a <code>Rectangle2D</code> that is the bounding box of the destination, given the
   * specified <code>src</code>
   */
  public final Rectangle2D getBounds2D(BufferedImage src) {
    return getBounds2D(src.getRaster());
  }

  /**
   * Returns the bounding box of the destination, given this source.
   * Note that this will be the same as the the bounding box of the
   * source.
   *
   * @param src the source <code>Raster</code>
   * @return a <code>Rectangle2D</code> that is the bounding box of the destination, given the
   * specified <code>src</code>
   */
  public final Rectangle2D getBounds2D(Raster src) {
        /*        return new Rectangle (src.getXOffset(),
                              src.getYOffset(),
                              src.getWidth(), src.getHeight()); */
    return src.getBounds();
  }

  /**
   * Creates a zeroed destination image with the correct size and number of
   * bands, given this source.
   *
   * @param src Source image for the filter operation.
   * @param destCM ColorModel of the destination.  If null, an appropriate ColorModel will be used.
   * @return a <code>BufferedImage</code> with the correct size and number of bands from the
   * specified <code>src</code>.
   * @throws IllegalArgumentException if <code>destCM</code> is <code>null</code> and this
   * <code>ColorConvertOp</code> was created without any <code>ICC_Profile</code> or
   * <code>ColorSpace</code> defined for the destination
   */
  public BufferedImage createCompatibleDestImage(BufferedImage src,
      ColorModel destCM) {
    ColorSpace cs = null;
    ;
    if (destCM == null) {
      if (CSList == null) {
                /* ICC case */
        int nProfiles = profileList.length;
        if (nProfiles == 0) {
          throw new IllegalArgumentException(
              "Destination ColorSpace is undefined");
        }
        ICC_Profile destProfile = profileList[nProfiles - 1];
        cs = new ICC_ColorSpace(destProfile);
      } else {
                /* non-ICC case */
        int nSpaces = CSList.length;
        cs = CSList[nSpaces - 1];
      }
    }
    return createCompatibleDestImage(src, destCM, cs);
  }

  private BufferedImage createCompatibleDestImage(BufferedImage src,
      ColorModel destCM,
      ColorSpace destCS) {
    BufferedImage image;
    if (destCM == null) {
      ColorModel srcCM = src.getColorModel();
      int nbands = destCS.getNumComponents();
      boolean hasAlpha = srcCM.hasAlpha();
      if (hasAlpha) {
        nbands += 1;
      }
      int[] nbits = new int[nbands];
      for (int i = 0; i < nbands; i++) {
        nbits[i] = 8;
      }
      destCM = new ComponentColorModel(destCS, nbits, hasAlpha,
          srcCM.isAlphaPremultiplied(),
          srcCM.getTransparency(),
          DataBuffer.TYPE_BYTE);
    }
    int w = src.getWidth();
    int h = src.getHeight();
    image = new BufferedImage(destCM,
        destCM.createCompatibleWritableRaster(w, h),
        destCM.isAlphaPremultiplied(), null);
    return image;
  }


  /**
   * Creates a zeroed destination Raster with the correct size and number of
   * bands, given this source.
   *
   * @param src the specified <code>Raster</code>
   * @return a <code>WritableRaster</code> with the correct size and number of bands from the
   * specified <code>src</code>
   * @throws IllegalArgumentException if this <code>ColorConvertOp</code> was created without
   * sufficient information to define the <code>dst</code> and <code>src</code> color spaces
   */
  public WritableRaster createCompatibleDestRaster(Raster src) {
    int ncomponents;

    if (CSList != null) {
            /* non-ICC case */
      if (CSList.length != 2) {
        throw new IllegalArgumentException(
            "Destination ColorSpace is undefined");
      }
      ncomponents = CSList[1].getNumComponents();
    } else {
            /* ICC case */
      int nProfiles = profileList.length;
      if (nProfiles < 2) {
        throw new IllegalArgumentException(
            "Destination ColorSpace is undefined");
      }
      ncomponents = profileList[nProfiles - 1].getNumComponents();
    }

    WritableRaster dest =
        Raster.createInterleavedRaster(DataBuffer.TYPE_BYTE,
            src.getWidth(),
            src.getHeight(),
            ncomponents,
            new Point(src.getMinX(), src.getMinY()));
    return dest;
  }

  /**
   * Returns the location of the destination point given a
   * point in the source.  If <code>dstPt</code> is non-null,
   * it will be used to hold the return value.  Note that
   * for this class, the destination point will be the same
   * as the source point.
   *
   * @param srcPt the specified source <code>Point2D</code>
   * @param dstPt the destination <code>Point2D</code>
   * @return <code>dstPt</code> after setting its location to be the same as <code>srcPt</code>
   */
  public final Point2D getPoint2D(Point2D srcPt, Point2D dstPt) {
    if (dstPt == null) {
      dstPt = new Point2D.Float();
    }
    dstPt.setLocation(srcPt.getX(), srcPt.getY());

    return dstPt;
  }


  /**
   * Returns the RenderingIntent from the specified ICC Profile.
   */
  private int getRenderingIntent(ICC_Profile profile) {
    byte[] header = profile.getData(ICC_Profile.icSigHead);
    int index = ICC_Profile.icHdrRenderingIntent;

        /* According to ICC spec, only the least-significant 16 bits shall be
         * used to encode the rendering intent. The most significant 16 bits
         * shall be set to zero. Thus, we are ignoring two most significant
         * bytes here.
         *
         *  See http://www.color.org/ICC1v42_2006-05.pdf, section 7.2.15.
         */
    return ((header[index + 2] & 0xff) << 8) |
        (header[index + 3] & 0xff);
  }

  /**
   * Returns the rendering hints used by this op.
   *
   * @return the <code>RenderingHints</code> object of this <code>ColorConvertOp</code>
   */
  public final RenderingHints getRenderingHints() {
    return hints;
  }

  private final BufferedImage nonICCBIFilter(BufferedImage src,
      ColorSpace srcColorSpace,
      BufferedImage dst,
      ColorSpace dstColorSpace) {

    int w = src.getWidth();
    int h = src.getHeight();
    ICC_ColorSpace ciespace =
        (ICC_ColorSpace) ColorSpace.getInstance(ColorSpace.CS_CIEXYZ);
    if (dst == null) {
      dst = createCompatibleDestImage(src, null);
      dstColorSpace = dst.getColorModel().getColorSpace();
    } else {
      if ((h != dst.getHeight()) || (w != dst.getWidth())) {
        throw new IllegalArgumentException(
            "Width or height of BufferedImages do not match");
      }
    }
    Raster srcRas = src.getRaster();
    WritableRaster dstRas = dst.getRaster();
    ColorModel srcCM = src.getColorModel();
    ColorModel dstCM = dst.getColorModel();
    int srcNumComp = srcCM.getNumColorComponents();
    int dstNumComp = dstCM.getNumColorComponents();
    boolean dstHasAlpha = dstCM.hasAlpha();
    boolean needSrcAlpha = srcCM.hasAlpha() && dstHasAlpha;
    ColorSpace[] list;
    if ((CSList == null) && (profileList.length != 0)) {
            /* possible non-ICC src, some profiles, possible non-ICC dst */
      boolean nonICCSrc, nonICCDst;
      ICC_Profile srcProfile, dstProfile;
      if (!(srcColorSpace instanceof ICC_ColorSpace)) {
        nonICCSrc = true;
        srcProfile = ciespace.getProfile();
      } else {
        nonICCSrc = false;
        srcProfile = ((ICC_ColorSpace) srcColorSpace).getProfile();
      }
      if (!(dstColorSpace instanceof ICC_ColorSpace)) {
        nonICCDst = true;
        dstProfile = ciespace.getProfile();
      } else {
        nonICCDst = false;
        dstProfile = ((ICC_ColorSpace) dstColorSpace).getProfile();
      }
            /* make a new transform if needed */
      if ((thisTransform == null) || (thisSrcProfile != srcProfile) ||
          (thisDestProfile != dstProfile)) {
        updateBITransform(srcProfile, dstProfile);
      }
      // process per scanline
      float maxNum = 65535.0f; // use 16-bit precision in CMM
      ColorSpace cs;
      int iccSrcNumComp;
      if (nonICCSrc) {
        cs = ciespace;
        iccSrcNumComp = 3;
      } else {
        cs = srcColorSpace;
        iccSrcNumComp = srcNumComp;
      }
      float[] srcMinVal = new float[iccSrcNumComp];
      float[] srcInvDiffMinMax = new float[iccSrcNumComp];
      for (int i = 0; i < srcNumComp; i++) {
        srcMinVal[i] = cs.getMinValue(i);
        srcInvDiffMinMax[i] = maxNum / (cs.getMaxValue(i) - srcMinVal[i]);
      }
      int iccDstNumComp;
      if (nonICCDst) {
        cs = ciespace;
        iccDstNumComp = 3;
      } else {
        cs = dstColorSpace;
        iccDstNumComp = dstNumComp;
      }
      float[] dstMinVal = new float[iccDstNumComp];
      float[] dstDiffMinMax = new float[iccDstNumComp];
      for (int i = 0; i < dstNumComp; i++) {
        dstMinVal[i] = cs.getMinValue(i);
        dstDiffMinMax[i] = (cs.getMaxValue(i) - dstMinVal[i]) / maxNum;
      }
      float[] dstColor;
      if (dstHasAlpha) {
        int size = ((dstNumComp + 1) > 3) ? (dstNumComp + 1) : 3;
        dstColor = new float[size];
      } else {
        int size = (dstNumComp > 3) ? dstNumComp : 3;
        dstColor = new float[size];
      }
      short[] srcLine = new short[w * iccSrcNumComp];
      short[] dstLine = new short[w * iccDstNumComp];
      Object pixel;
      float[] color;
      float[] alpha = null;
      if (needSrcAlpha) {
        alpha = new float[w];
      }
      int idx;
      // process each scanline
      for (int y = 0; y < h; y++) {
        // convert src scanline
        pixel = null;
        color = null;
        idx = 0;
        for (int x = 0; x < w; x++) {
          pixel = srcRas.getDataElements(x, y, pixel);
          color = srcCM.getNormalizedComponents(pixel, color, 0);
          if (needSrcAlpha) {
            alpha[x] = color[srcNumComp];
          }
          if (nonICCSrc) {
            color = srcColorSpace.toCIEXYZ(color);
          }
          for (int i = 0; i < iccSrcNumComp; i++) {
            srcLine[idx++] = (short)
                ((color[i] - srcMinVal[i]) * srcInvDiffMinMax[i] +
                    0.5f);
          }
        }
        // color convert srcLine to dstLine
        thisTransform.colorConvert(srcLine, dstLine);
        // convert dst scanline
        pixel = null;
        idx = 0;
        for (int x = 0; x < w; x++) {
          for (int i = 0; i < iccDstNumComp; i++) {
            dstColor[i] = ((float) (dstLine[idx++] & 0xffff)) *
                dstDiffMinMax[i] + dstMinVal[i];
          }
          if (nonICCDst) {
            color = srcColorSpace.fromCIEXYZ(dstColor);
            for (int i = 0; i < dstNumComp; i++) {
              dstColor[i] = color[i];
            }
          }
          if (needSrcAlpha) {
            dstColor[dstNumComp] = alpha[x];
          } else if (dstHasAlpha) {
            dstColor[dstNumComp] = 1.0f;
          }
          pixel = dstCM.getDataElements(dstColor, 0, pixel);
          dstRas.setDataElements(x, y, pixel);
        }
      }
    } else {
            /* possible non-ICC src, possible CSList, possible non-ICC dst */
      // process per pixel
      int numCS;
      if (CSList == null) {
        numCS = 0;
      } else {
        numCS = CSList.length;
      }
      float[] dstColor;
      if (dstHasAlpha) {
        dstColor = new float[dstNumComp + 1];
      } else {
        dstColor = new float[dstNumComp];
      }
      Object spixel = null;
      Object dpixel = null;
      float[] color = null;
      float[] tmpColor;
      // process each pixel
      for (int y = 0; y < h; y++) {
        for (int x = 0; x < w; x++) {
          spixel = srcRas.getDataElements(x, y, spixel);
          color = srcCM.getNormalizedComponents(spixel, color, 0);
          tmpColor = srcColorSpace.toCIEXYZ(color);
          for (int i = 0; i < numCS; i++) {
            tmpColor = CSList[i].fromCIEXYZ(tmpColor);
            tmpColor = CSList[i].toCIEXYZ(tmpColor);
          }
          tmpColor = dstColorSpace.fromCIEXYZ(tmpColor);
          for (int i = 0; i < dstNumComp; i++) {
            dstColor[i] = tmpColor[i];
          }
          if (needSrcAlpha) {
            dstColor[dstNumComp] = color[srcNumComp];
          } else if (dstHasAlpha) {
            dstColor[dstNumComp] = 1.0f;
          }
          dpixel = dstCM.getDataElements(dstColor, 0, dpixel);
          dstRas.setDataElements(x, y, dpixel);

        }
      }
    }

    return dst;
  }

  /* color convert a Raster - handles byte, ushort, int, short, float,
     or double transferTypes */
  private final WritableRaster nonICCRasterFilter(Raster src,
      WritableRaster dst) {

    if (CSList.length != 2) {
      throw new IllegalArgumentException(
          "Destination ColorSpace is undefined");
    }
    if (src.getNumBands() != CSList[0].getNumComponents()) {
      throw new IllegalArgumentException(
          "Numbers of source Raster bands and source color space " +
              "components do not match");
    }
    if (dst == null) {
      dst = createCompatibleDestRaster(src);
    } else {
      if (src.getHeight() != dst.getHeight() ||
          src.getWidth() != dst.getWidth()) {
        throw new IllegalArgumentException(
            "Width or height of Rasters do not match");
      }
      if (dst.getNumBands() != CSList[1].getNumComponents()) {
        throw new IllegalArgumentException(
            "Numbers of destination Raster bands and destination " +
                "color space components do not match");
      }
    }

    if (srcMinVals == null) {
      getMinMaxValsFromColorSpaces(CSList[0], CSList[1]);
    }

    SampleModel srcSM = src.getSampleModel();
    SampleModel dstSM = dst.getSampleModel();
    boolean srcIsFloat, dstIsFloat;
    int srcTransferType = src.getTransferType();
    int dstTransferType = dst.getTransferType();
    if ((srcTransferType == DataBuffer.TYPE_FLOAT) ||
        (srcTransferType == DataBuffer.TYPE_DOUBLE)) {
      srcIsFloat = true;
    } else {
      srcIsFloat = false;
    }
    if ((dstTransferType == DataBuffer.TYPE_FLOAT) ||
        (dstTransferType == DataBuffer.TYPE_DOUBLE)) {
      dstIsFloat = true;
    } else {
      dstIsFloat = false;
    }
    int w = src.getWidth();
    int h = src.getHeight();
    int srcNumBands = src.getNumBands();
    int dstNumBands = dst.getNumBands();
    float[] srcScaleFactor = null;
    float[] dstScaleFactor = null;
    if (!srcIsFloat) {
      srcScaleFactor = new float[srcNumBands];
      for (int i = 0; i < srcNumBands; i++) {
        if (srcTransferType == DataBuffer.TYPE_SHORT) {
          srcScaleFactor[i] = (srcMaxVals[i] - srcMinVals[i]) /
              32767.0f;
        } else {
          srcScaleFactor[i] = (srcMaxVals[i] - srcMinVals[i]) /
              ((float) ((1 << srcSM.getSampleSize(i)) - 1));
        }
      }
    }
    if (!dstIsFloat) {
      dstScaleFactor = new float[dstNumBands];
      for (int i = 0; i < dstNumBands; i++) {
        if (dstTransferType == DataBuffer.TYPE_SHORT) {
          dstScaleFactor[i] = 32767.0f /
              (dstMaxVals[i] - dstMinVals[i]);
        } else {
          dstScaleFactor[i] =
              ((float) ((1 << dstSM.getSampleSize(i)) - 1)) /
                  (dstMaxVals[i] - dstMinVals[i]);
        }
      }
    }
    int ys = src.getMinY();
    int yd = dst.getMinY();
    int xs, xd;
    float sample;
    float[] color = new float[srcNumBands];
    float[] tmpColor;
    ColorSpace srcColorSpace = CSList[0];
    ColorSpace dstColorSpace = CSList[1];
    // process each pixel
    for (int y = 0; y < h; y++, ys++, yd++) {
      // get src scanline
      xs = src.getMinX();
      xd = dst.getMinX();
      for (int x = 0; x < w; x++, xs++, xd++) {
        for (int i = 0; i < srcNumBands; i++) {
          sample = src.getSampleFloat(xs, ys, i);
          if (!srcIsFloat) {
            sample = sample * srcScaleFactor[i] + srcMinVals[i];
          }
          color[i] = sample;
        }
        tmpColor = srcColorSpace.toCIEXYZ(color);
        tmpColor = dstColorSpace.fromCIEXYZ(tmpColor);
        for (int i = 0; i < dstNumBands; i++) {
          sample = tmpColor[i];
          if (!dstIsFloat) {
            sample = (sample - dstMinVals[i]) * dstScaleFactor[i];
          }
          dst.setSample(xd, yd, i, sample);
        }
      }
    }
    return dst;
  }

  private void getMinMaxValsFromProfiles(ICC_Profile srcProfile,
      ICC_Profile dstProfile) {
    int type = srcProfile.getColorSpaceType();
    int nc = srcProfile.getNumComponents();
    srcMinVals = new float[nc];
    srcMaxVals = new float[nc];
    setMinMax(type, nc, srcMinVals, srcMaxVals);
    type = dstProfile.getColorSpaceType();
    nc = dstProfile.getNumComponents();
    dstMinVals = new float[nc];
    dstMaxVals = new float[nc];
    setMinMax(type, nc, dstMinVals, dstMaxVals);
  }

  private void setMinMax(int type, int nc, float[] minVals, float[] maxVals) {
    if (type == ColorSpace.TYPE_Lab) {
      minVals[0] = 0.0f;    // L
      maxVals[0] = 100.0f;
      minVals[1] = -128.0f; // a
      maxVals[1] = 127.0f;
      minVals[2] = -128.0f; // b
      maxVals[2] = 127.0f;
    } else if (type == ColorSpace.TYPE_XYZ) {
      minVals[0] = minVals[1] = minVals[2] = 0.0f; // X, Y, Z
      maxVals[0] = maxVals[1] = maxVals[2] = 1.0f + (32767.0f / 32768.0f);
    } else {
      for (int i = 0; i < nc; i++) {
        minVals[i] = 0.0f;
        maxVals[i] = 1.0f;
      }
    }
  }

  private void getMinMaxValsFromColorSpaces(ColorSpace srcCspace,
      ColorSpace dstCspace) {
    int nc = srcCspace.getNumComponents();
    srcMinVals = new float[nc];
    srcMaxVals = new float[nc];
    for (int i = 0; i < nc; i++) {
      srcMinVals[i] = srcCspace.getMinValue(i);
      srcMaxVals[i] = srcCspace.getMaxValue(i);
    }
    nc = dstCspace.getNumComponents();
    dstMinVals = new float[nc];
    dstMaxVals = new float[nc];
    for (int i = 0; i < nc; i++) {
      dstMinVals[i] = dstCspace.getMinValue(i);
      dstMaxVals[i] = dstCspace.getMaxValue(i);
    }
  }

}
